﻿/********************
 * MD2 Model Animator R2.0
 * 
 * Author(s): Matthew Lynch
 * 
 * Copyright:
 * 
 * This class is released under the 
 * Creative Commons Attribution-Share Alike 3.0 License. 
 * 
 * For more information please see:
 * http://creativecommons.org/licenses/by-sa/3.0/
 * 
 ********************/

#region Standard Using Statements
using System;
using System.IO;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Storage;
#endregion

namespace XnaMD2
{

    public class MD2Model
    {

        private const int m_MD2ValidID = ('I' + ('D' << 8) + ('P' << 16) + ('2' << 24));

        #region Stored Model Structures

        /***
         * These structures store the 'raw' MD2 model data, prior to the 
         * conversion needed to render the model in an XNA space.
         ***/

        private struct MD2FileHeader
        {
            // File identification information.
            public int ID;
            public int Version;

            // Texture dimensions.
            public int SkinWidth;
            public int SkinHeight;

            // How many bytes in each frame.
            public int FrameSize;

            // How many of each type of object.
            public int CountVertices; // How many vertices per frame. (Other counts are total for model.)
            public int CountTriangles;
            public int CountFrames;
            public int CountSkins;
            public int CountTextureMaps;
            public int CountOGLCommands;

            // Offsets to where data starts in the model file.
            public int OffsetTriangles;
            public int OffsetFrames;
            public int OffsetSkins;
            public int OffsetTextureMaps;
            public int OffsetOGLCommands;
            public int OffsetEOF;
        }

        private struct MD2Skin
        {
            public char[] TexturePath; // Path to texture image. Not used here yet, see release notes.
        }

        private struct MD2TextureCoordinate
        {
            public short X; // Compressed X ('U') texture coordinate.
            public short Y; // Compressed Y ('V') texture coordinate.
        }

        private struct MD2Triangle
        {
            public ushort[] Vertex; // Index into the model's vertex arrays.
            public ushort[] TextureCoordinate; // Index into the model's texture coordinates array.
        }

        private struct MD2Vertex
        {
            public byte[] Coordinate; // Compressed vector coordinates.
            public byte LightingNormal; // Index into Quake II Lighting Normals table. Not used here yet, see release notes.
        }

        private struct MD2Frame
        {
            public char[] Name; // Name of frame, useful for identifying animation sets.
            public float[] Scale; // Scale data for decompression of vertices data.
            public float[] Translation; // Translation data for decompression of vertices data.
            public MD2Vertex[] Vertex; // Compressed vertex data for frame.
            public Vector3[] DecompressedVertices;
        }

        #endregion

        private MD2FileHeader m_MD2FileHeader;
        private MD2Skin[] m_MD2Skins;
        private MD2TextureCoordinate[] m_MD2TextureMaps;
        private MD2Triangle[] m_MD2Triangles;
        private MD2Frame[] m_MD2Frames;

        private struct TranslatedFrame
        {
            public string Name;
            public VertexPositionColorTexture[] VertexBuffer;
        }

        private struct MD2Animation
        {
            public string Name;
            public int StartFrame;
            public int FinishFrame;
        }

        private TranslatedFrame[] m_Frames;
        private Dictionary<string, MD2Animation> m_Animations;

        private Vector3 m_Position;

        private float m_Scale;
        private float m_Rotation;
        private float m_RotationOffset;

        private Matrix m_WorldMatrix;
        private Matrix m_ScaleMatrix;
        private Matrix m_RotationMatrix;

        private bool m_FirstBufferCall = true;

        private string m_CurrentAnimation;
        private int m_CurrentFrame;
        private double m_AnimationClock;
        private int m_FrameRate;

        public int TriangleCount
        {
            get { return m_MD2FileHeader.CountTriangles; }
        }

        public string CurrentAnimation
        {
            get { return m_CurrentAnimation; }
            set
            {
                if (m_Animations.ContainsKey(value))
                {
                    m_CurrentAnimation = value;
                }
                else
                {
                    throw new KeyNotFoundException("No animation of the name '" + value + "' was found in the model.");
                }
            }
        }

        public int FrameRate
        {
            get { return m_FrameRate; }
            set { m_FrameRate = value; }
        }

        public int CurrentFrame
        {
            get { return m_CurrentFrame; }
            set { m_CurrentFrame = value; }
        }

        public Matrix WorldMatrix
        {
            get { return m_WorldMatrix; }
        }

        public Vector3 Position
        {
            get { return m_Position; }
            set
            {
                m_Position = value;
                m_WorldMatrix = m_ScaleMatrix * m_RotationMatrix * Matrix.CreateTranslation(m_Position);
            }
        }

        public float Scale
        {
            get { return this.m_Scale; }
            set
            {
                this.m_Scale = value;
                this.m_ScaleMatrix = Matrix.CreateScale(Scale / 100f);
                m_WorldMatrix = m_ScaleMatrix * m_RotationMatrix * Matrix.CreateTranslation(m_Position);
            }
        }

        public float RotationOffset
        {
            get { return this.m_RotationOffset; }
            set
            {
                m_RotationOffset = value;
                Rotation = 0;
            }
        }

        public float Rotation
        {
            get { return this.m_Rotation; }
            set
            {
                m_Rotation = value;

                if (m_Rotation < 0) { m_Rotation = 360; }
                if (m_Rotation > 360) { m_Rotation = 0; }

                m_RotationMatrix = Matrix.CreateFromAxisAngle(Vector3.Up, MathHelper.ToRadians(m_Rotation)) * Matrix.CreateFromAxisAngle(Vector3.Up, MathHelper.ToRadians(-m_RotationOffset));
                m_WorldMatrix = m_ScaleMatrix * m_RotationMatrix * Matrix.CreateTranslation(m_Position);
            }
        }

        public MD2Model()
        {
            m_Animations = new Dictionary<string, MD2Animation>();
            m_AnimationClock = 0;
            m_FrameRate = 12;

            Scale = 100;
            RotationOffset = 0;
            Rotation = 0;
        }

        public MD2Model(MD2Model model)
        {
            m_MD2FileHeader = model.m_MD2FileHeader;
            m_MD2Skins = model.m_MD2Skins;
            m_MD2TextureMaps = model.m_MD2TextureMaps;
            m_MD2Triangles = model.m_MD2Triangles;
            m_MD2Frames = model.m_MD2Frames;
            m_Frames = model.m_Frames;
            m_Animations = model.m_Animations;
            m_AnimationClock = 0;
            m_FrameRate = model.m_FrameRate;

            m_Position = model.m_Position;
            m_Scale = model.m_Scale;
            RotationOffset = model.m_RotationOffset;
            Rotation = model.m_Rotation;
            m_WorldMatrix = model.m_WorldMatrix;
            m_ScaleMatrix = model.m_ScaleMatrix;
            m_RotationMatrix = model.m_RotationMatrix;
        }

        public bool LoadFromFile(string path)
        {
            m_Animations.Clear();
            m_FirstBufferCall = true;

            System.IO.FileStream stream = null;
            System.IO.BinaryReader reader = null;

            try
            {
                // Open the specified file as a stream.
                stream = new System.IO.FileStream(path, FileMode.Open, FileAccess.Read);

                // Pipe the stream in to a binary reader so we can work at the byte-by-byte level.
                reader = new System.IO.BinaryReader(stream);

                /*** LOAD FILE HEADER ***/

                this.m_MD2FileHeader = new MD2FileHeader();

                // If the file is a valid MD2 model the first 4B of data will be an ID.
                // The ID is a standard integer which should match _MD2VALIDID.

                this.m_MD2FileHeader.ID = reader.ReadInt32();

                if (this.m_MD2FileHeader.ID == m_MD2ValidID)
                {

                    /*** LOAD HEADER DATA ***/

                    // After the magic number the next 64B of data contains header information.
                    // For more information see the Struct MD2Header definition.

                    this.m_MD2FileHeader.Version = reader.ReadInt32();

                    this.m_MD2FileHeader.SkinWidth = reader.ReadInt32();
                    this.m_MD2FileHeader.SkinHeight = reader.ReadInt32();

                    this.m_MD2FileHeader.FrameSize = reader.ReadInt32();

                    this.m_MD2FileHeader.CountSkins = reader.ReadInt32();
                    this.m_MD2FileHeader.CountVertices = reader.ReadInt32();
                    this.m_MD2FileHeader.CountTextureMaps = reader.ReadInt32();
                    this.m_MD2FileHeader.CountTriangles = reader.ReadInt32();
                    this.m_MD2FileHeader.CountOGLCommands = reader.ReadInt32();
                    this.m_MD2FileHeader.CountFrames = reader.ReadInt32();

                    this.m_MD2FileHeader.OffsetSkins = reader.ReadInt32();
                    this.m_MD2FileHeader.OffsetTextureMaps = reader.ReadInt32();
                    this.m_MD2FileHeader.OffsetTriangles = reader.ReadInt32();
                    this.m_MD2FileHeader.OffsetFrames = reader.ReadInt32();
                    this.m_MD2FileHeader.OffsetOGLCommands = reader.ReadInt32();
                    this.m_MD2FileHeader.OffsetEOF = reader.ReadInt32();

                    /*** LOAD SKIN DEFINITIONS ***/

                    // Initialise data array.
                    this.m_MD2Skins = new MD2Skin[this.m_MD2FileHeader.CountSkins];

                    // Jump to the position in file where data starts.
                    // This is defined in the header data.
                    reader.BaseStream.Seek(this.m_MD2FileHeader.OffsetSkins, SeekOrigin.Begin);

                    // Loop for each entry.
                    for (int i = 0; i < this.m_MD2FileHeader.CountSkins; i++)
                    {

                        this.m_MD2Skins[i].TexturePath = reader.ReadChars(64);

                    }

                    /*** LOAD TEXTURE MAPS ***/

                    this.m_MD2TextureMaps = new MD2TextureCoordinate[this.m_MD2FileHeader.CountTextureMaps];

                    reader.BaseStream.Seek(this.m_MD2FileHeader.OffsetTextureMaps, SeekOrigin.Begin);

                    for (int i = 0; i < this.m_MD2FileHeader.CountTextureMaps; i++)
                    {

                        this.m_MD2TextureMaps[i].X = reader.ReadInt16();
                        this.m_MD2TextureMaps[i].Y = reader.ReadInt16();

                    }

                    /*** LOAD TRIANGLE DATA ***/

                    this.m_MD2Triangles = new MD2Triangle[this.m_MD2FileHeader.CountTriangles];

                    reader.BaseStream.Seek(this.m_MD2FileHeader.OffsetTriangles, SeekOrigin.Begin);

                    for (int i = 0; i < this.m_MD2FileHeader.CountTriangles; i++)
                    {

                        this.m_MD2Triangles[i].Vertex = new ushort[3];
                        this.m_MD2Triangles[i].Vertex[0] = reader.ReadUInt16();
                        this.m_MD2Triangles[i].Vertex[1] = reader.ReadUInt16();
                        this.m_MD2Triangles[i].Vertex[2] = reader.ReadUInt16();

                        this.m_MD2Triangles[i].TextureCoordinate = new ushort[3];
                        this.m_MD2Triangles[i].TextureCoordinate[0] = reader.ReadUInt16();
                        this.m_MD2Triangles[i].TextureCoordinate[1] = reader.ReadUInt16();
                        this.m_MD2Triangles[i].TextureCoordinate[2] = reader.ReadUInt16();

                    }

                    /*** LOAD FRAMES ***/

                    this.m_MD2Frames = new MD2Frame[this.m_MD2FileHeader.CountFrames];

                    for (int i = 0; i < this.m_MD2FileHeader.CountFrames; i++)
                    {

                        // Unlike other data, frame data is of a defined size not fixed.
                        // As such the jump to the start of the data must happen for each frame.
                        reader.BaseStream.Seek((this.m_MD2FileHeader.OffsetFrames + (i * this.m_MD2FileHeader.FrameSize)), SeekOrigin.Begin);

                        this.m_MD2Frames[i].Scale = new float[3];
                        this.m_MD2Frames[i].Translation = new float[3];

                        // The binary reader does not read floats; 'scale' and 'translate' are foat[]..
                        // We need to read the data to a byte[] and convert it.
                        byte[] buffer;

                        buffer = reader.ReadBytes(4);
                        this.m_MD2Frames[i].Scale[0] = System.BitConverter.ToSingle(buffer, 0);
                        buffer = reader.ReadBytes(4);
                        this.m_MD2Frames[i].Scale[1] = System.BitConverter.ToSingle(buffer, 0);
                        buffer = reader.ReadBytes(4);
                        this.m_MD2Frames[i].Scale[2] = System.BitConverter.ToSingle(buffer, 0);

                        buffer = reader.ReadBytes(4);
                        this.m_MD2Frames[i].Translation[0] = System.BitConverter.ToSingle(buffer, 0);
                        buffer = reader.ReadBytes(4);
                        this.m_MD2Frames[i].Translation[1] = System.BitConverter.ToSingle(buffer, 0);
                        buffer = reader.ReadBytes(4);
                        this.m_MD2Frames[i].Translation[2] = System.BitConverter.ToSingle(buffer, 0);

                        this.m_MD2Frames[i].Name = reader.ReadChars(16);

                        /*** LOAD VERTEX DATA (FOR FRAME) ***/

                        this.m_MD2Frames[i].Vertex = new MD2Vertex[this.m_MD2FileHeader.CountVertices];
                        this.m_MD2Frames[i].DecompressedVertices = new Vector3[this.m_MD2FileHeader.CountVertices];

                        for (int j = 0; j < this.m_MD2FileHeader.CountVertices; j++)
                        {
                            this.m_MD2Frames[i].Vertex[j].Coordinate = reader.ReadBytes(3);
                            this.m_MD2Frames[i].Vertex[j].LightingNormal = reader.ReadByte();
                        }
                    }

                    /*** DECOMPRESS AND TRANSLATE FRAMES ***/

                    this.m_Frames = new TranslatedFrame[this.m_MD2FileHeader.CountFrames];

                    for (int i = 0; i < this.m_MD2FileHeader.CountFrames; i++)
                    {
                        this.m_Frames[i].Name = new string(this.m_MD2Frames[i].Name);
                        this.m_Frames[i].VertexBuffer = new VertexPositionColorTexture[this.m_MD2FileHeader.CountTriangles * 3];

                        int j = 0; // position in vertex buffer

                        for (int k = 0; k < this.m_MD2FileHeader.CountTriangles; k++)
                        {

                            // We need to invert the vertices of the triangle,
                            // switch the Y and Z coordinates of the vertices,
                            // decompress the vertices coordinates (coord * scale + translation) and
                            // convert the texture coordinates (x / width, y / height).

                            this.m_Frames[i].VertexBuffer[j].Position.X = this.m_MD2Frames[i].Vertex[this.m_MD2Triangles[k].Vertex[2]].Coordinate[0] * this.m_MD2Frames[i].Scale[0] + this.m_MD2Frames[i].Translation[0];
                            this.m_Frames[i].VertexBuffer[j].Position.Y = this.m_MD2Frames[i].Vertex[this.m_MD2Triangles[k].Vertex[2]].Coordinate[2] * this.m_MD2Frames[i].Scale[2] + this.m_MD2Frames[i].Translation[2];
                            this.m_Frames[i].VertexBuffer[j].Position.Z = this.m_MD2Frames[i].Vertex[this.m_MD2Triangles[k].Vertex[2]].Coordinate[1] * this.m_MD2Frames[i].Scale[1] + this.m_MD2Frames[i].Translation[1];

                            this.m_Frames[i].VertexBuffer[j].TextureCoordinate.X = (float)this.m_MD2TextureMaps[this.m_MD2Triangles[k].TextureCoordinate[2]].X / (float)this.m_MD2FileHeader.SkinWidth;
                            this.m_Frames[i].VertexBuffer[j].TextureCoordinate.Y = (float)this.m_MD2TextureMaps[this.m_MD2Triangles[k].TextureCoordinate[2]].Y / (float)this.m_MD2FileHeader.SkinHeight;

                            j += 1;

                            this.m_Frames[i].VertexBuffer[j].Position.X = this.m_MD2Frames[i].Vertex[this.m_MD2Triangles[k].Vertex[1]].Coordinate[0] * this.m_MD2Frames[i].Scale[0] + this.m_MD2Frames[i].Translation[0];
                            this.m_Frames[i].VertexBuffer[j].Position.Y = this.m_MD2Frames[i].Vertex[this.m_MD2Triangles[k].Vertex[1]].Coordinate[2] * this.m_MD2Frames[i].Scale[2] + this.m_MD2Frames[i].Translation[2];
                            this.m_Frames[i].VertexBuffer[j].Position.Z = this.m_MD2Frames[i].Vertex[this.m_MD2Triangles[k].Vertex[1]].Coordinate[1] * this.m_MD2Frames[i].Scale[1] + this.m_MD2Frames[i].Translation[1];

                            this.m_Frames[i].VertexBuffer[j].TextureCoordinate.X = (float)this.m_MD2TextureMaps[this.m_MD2Triangles[k].TextureCoordinate[1]].X / (float)this.m_MD2FileHeader.SkinWidth;
                            this.m_Frames[i].VertexBuffer[j].TextureCoordinate.Y = (float)this.m_MD2TextureMaps[this.m_MD2Triangles[k].TextureCoordinate[1]].Y / (float)this.m_MD2FileHeader.SkinHeight;

                            j += 1;

                            this.m_Frames[i].VertexBuffer[j].Position.X = this.m_MD2Frames[i].Vertex[this.m_MD2Triangles[k].Vertex[0]].Coordinate[0] * this.m_MD2Frames[i].Scale[0] + this.m_MD2Frames[i].Translation[0];
                            this.m_Frames[i].VertexBuffer[j].Position.Y = this.m_MD2Frames[i].Vertex[this.m_MD2Triangles[k].Vertex[0]].Coordinate[2] * this.m_MD2Frames[i].Scale[2] + this.m_MD2Frames[i].Translation[2];
                            this.m_Frames[i].VertexBuffer[j].Position.Z = this.m_MD2Frames[i].Vertex[this.m_MD2Triangles[k].Vertex[0]].Coordinate[1] * this.m_MD2Frames[i].Scale[1] + this.m_MD2Frames[i].Translation[1];

                            this.m_Frames[i].VertexBuffer[j].TextureCoordinate.X = (float)this.m_MD2TextureMaps[this.m_MD2Triangles[k].TextureCoordinate[0]].X / (float)this.m_MD2FileHeader.SkinWidth;
                            this.m_Frames[i].VertexBuffer[j].TextureCoordinate.Y = (float)this.m_MD2TextureMaps[this.m_MD2Triangles[k].TextureCoordinate[0]].Y / (float)this.m_MD2FileHeader.SkinHeight;

                            j += 1;
                        }
                    }

                    string animationName = "";
                    string frameName = "";
                    int currentIndex = 1;

                    int start = 1;
                    int finish = 1;

                    for (int i = 0; i < m_Frames.Length; i++)
                    {
                        frameName = m_Frames[i].Name;
                        if (frameName.IndexOf("\0") > 0) { frameName = frameName.Substring(0, frameName.IndexOf("\0")); }

                        if (frameName.EndsWith("0") && !currentIndex.ToString().EndsWith("0")) { frameName = frameName.Substring(0, frameName.Length - 1); }

                        frameName = frameName.Substring(0, frameName.Length - currentIndex.ToString().Length);

                        if (frameName.EndsWith("0")) { frameName = frameName.Substring(0, frameName.Length - 1); }

                        if (frameName != animationName)
                        {
                            if (animationName != "" && frameName != "")
                            {
                                finish = i;

                                MD2Animation animation = new MD2Animation();

                                animation.Name = animationName;
                                animation.StartFrame = start;
                                animation.FinishFrame = finish;

                                m_Animations.Add(animationName, animation);
                            }

                            currentIndex = 1;

                            frameName = m_Frames[i].Name;
                            if (frameName.IndexOf("\0") > 0) { frameName = frameName.Substring(0, frameName.IndexOf("\0")); }

                            if (frameName.EndsWith("0") && !currentIndex.ToString().EndsWith("0")) { frameName = frameName.Substring(0, frameName.Length - 1); }

                            frameName = frameName.Substring(0, frameName.Length - currentIndex.ToString().Length);

                            if (frameName.EndsWith("0")) { frameName = frameName.Substring(0, frameName.Length - 1); }

                            animationName = frameName;
                            start = i + 1;
                            finish = i + 1;

                            currentIndex += 1;
                        }
                        else
                        {
                            currentIndex += 1;
                        }

                        if (i == m_Frames.Length - 1 && animationName != "" && frameName != "")
                        {
                            finish = i + 1;

                            MD2Animation animation = new MD2Animation();

                            animation.Name = animationName;
                            animation.StartFrame = start;
                            animation.FinishFrame = finish;

                            m_Animations.Add(animationName, animation);
                        }
                    }

                    return true;
                }
            }
            catch (Exception ex)
            {
                throw new Exception("Error while loading MD2 model from definition file ( " + path + " ).", ex);
            }
            finally
            {
                if (reader != null) { reader.Close(); }
                if (stream != null)
                {
                    stream.Close();
                    stream.Dispose();
                }
            }

            return false;
        }

        public VertexPositionColorTexture[] GetVertexBuffer(int frame)
        {
            return this.m_Frames[frame].VertexBuffer;
        }

        public VertexPositionColorTexture[] GetVertexBuffer(GameTime gameTime)
        {
            MD2Animation currentAnimation = m_Animations[m_CurrentAnimation];

            if (m_FirstBufferCall)
            {
                m_CurrentFrame = currentAnimation.StartFrame;
                m_AnimationClock = 0;
                m_FirstBufferCall = false;
            }

            int timePerFrame = 1500 / m_FrameRate;

            m_AnimationClock += gameTime.ElapsedGameTime.TotalMilliseconds;

            if (m_AnimationClock >= timePerFrame)
            {
                m_AnimationClock = 0;
                m_CurrentFrame++;
                m_CurrentFrame = (m_CurrentFrame >= currentAnimation.FinishFrame) ? currentAnimation.StartFrame : m_CurrentFrame;
            }

            return this.m_Frames[m_CurrentFrame].VertexBuffer;
        }
    }
}